import { Result, ok, err } from "neverthrow";
import {
  CLIENT_STRINGS as _$,
  CLIENT_STRINGS_REF as _$REF,
  ITEM_BY_REF,
  STAT_BY_MATCH_STR,
  BaseType,
  ITEM_BY_TRANSLATED,
} from "@/assets/data";
import { ModifierType, StatCalculated, sumStatsByModType } from "./modifiers";
import {
  linesToStatStrings,
  tryParseTranslation,
  getRollOrMinmaxAvg,
} from "./stat-translations";
import { ItemCategory } from "./meta";
import {
  IncursionRoom,
  ParsedItem,
  ItemInfluence,
  ItemRarity,
  itemIsModifiable,
} from "./ParsedItem";
import { magicBasetype } from "./magic-name";
import {
  // isModInfoLine,
  // groupLinesByMod,
  // parseModInfoLine,
  parseModType,
  ModifierInfo,
  ParsedModifier,
  ENCHANT_LINE,
  SCOURGE_LINE,
  IMPLICIT_LINE,
  RUNE_LINE,
  isModInfoLine,
  groupLinesByMod,
  parseModInfoLine,
  ADDED_RUNE_LINE,
} from "./advanced-mod-desc";
import { calcPropPercentile, QUALITY_STATS } from "./calc-q20";

type SectionParseResult =
  | "SECTION_PARSED"
  | "SECTION_SKIPPED"
  | "PARSER_SKIPPED";

type ParserFn = (section: string[], item: ParserState) => SectionParseResult;
type VirtualParserFn = (item: ParserState) => Result<never, string> | void;

interface ParserState extends ParsedItem {
  name: string;
  baseType: string | undefined;
  infoVariants: BaseType[];
}

const parsers: Array<ParserFn | { virtual: VirtualParserFn }> = [
  parseUnidentified,
  { virtual: parseSuperior },
  { virtual: parseExceptional },
  parseSynthesised,
  parseCategoryByHelpText,
  { virtual: normalizeName },
  parseVaalGemName,
  { virtual: findInDatabase },
  // -----------
  parseItemLevel,
  parseRequirements,
  parseTalismanTier,
  parseGem,
  parseArmour,
  parseWeapon,
  parseCaster,
  parseFlask,
  parseJewelery,
  parseCharmSlots,
  parseSpirit,
  parsePriceNote,
  parseUnneededText,
  parseFracturedText,
  parseTimelostRadius,
  parseStackSize,
  parseCorrupted,
  parseFoil,
  parseInfluence,
  parseMap,
  parseWaystone,
  parseSockets,
  parseRuneSockets,
  parseHeistBlueprint,
  parseAreaLevel,
  parseAtzoatlRooms,
  parseMirroredTablet,
  parseFilledCoffin,
  parseMirrored,
  parseSanctified,
  parseSentinelCharge,
  parseLogbookArea,
  parseLogbookArea,
  parseLogbookArea,
  parseModifiers, // enchant
  parseModifiers, // rune
  parseModifiers, // implicit
  parseModifiers, // grant skill
  parseModifiers, // explicit
  // catch enchant and rune since they don't have curlys rn
  parseModifiersPoe2, // enchant
  parseModifiersPoe2, // rune
  // HACK: catch implicit and explicit for controllers
  parseModifiersPoe2, // implicit
  parseModifiersPoe2, // grant skill
  parseModifiersPoe2, // explicit
  { virtual: transformToLegacyModifiers },
  { virtual: parseFractured },
  { virtual: parseBlightedMap },
  { virtual: applyRuneSockets },
  { virtual: applyElementalAdded },
  { virtual: pickCorrectVariant },
  { virtual: calcBasePercentile },
];

export function parseClipboard(clipboard: string): Result<ParsedItem, string> {
  try {
    let sections = itemTextToSections(clipboard);

    if (sections[0][2] === _$.CANNOT_USE_ITEM) {
      sections[0].pop(); // remove CANNOT_USE_ITEM line
      sections[1].unshift(...sections[0]); // prepend item class & rarity into second section
      sections.shift(); // remove first section where CANNOT_USE_ITEM line was
    }
    const parsed = parseNamePlate(sections[0]);
    if (!parsed.isOk()) return parsed;

    sections.shift();
    parsed.value.rawText = clipboard;

    // each section can be parsed at most by one parser
    // and each parser can only be used to parse one section
    for (const parser of parsers) {
      if (typeof parser === "object") {
        const error = parser.virtual(parsed.value);
        if (error) return error;
        continue;
      }
      for (const section of sections) {
        const result = parser(section, parsed.value);
        if (result === "SECTION_PARSED") {
          sections = sections.filter((s) => s !== section);
          break;
        } else if (result === "PARSER_SKIPPED") {
          break;
        }
      }
    }
    return Object.freeze(parsed);
  } catch (e) {
    console.log(e);
    return err("item.parse_error");
  }
}

function itemTextToSections(text: string) {
  const lines = text.split(/\r?\n/);
  if (lines[lines.length - 1] === "") {
    lines.pop();
  }

  const sections: string[][] = [[]];
  lines.reduce((section, line) => {
    if (line !== "--------") {
      section.push(line);
      return section;
    } else {
      const section: string[] = [];
      sections.push(section);
      return section;
    }
  }, sections[0]);
  return sections.filter((section) => section.length);
}

function normalizeName(item: ParserState) {
  if (item.rarity === ItemRarity.Magic) {
    const baseType = magicBasetype(item.name);
    if (baseType) {
      item.name = baseType;
    }
  }

  if (item.rarity === ItemRarity.Normal || item.rarity === ItemRarity.Rare) {
    if (item.baseType) {
      if (_$.MAP_BLIGHTED.test(item.baseType)) {
        item.baseType = _$REF.MAP_BLIGHTED.exec(item.baseType)![1];
      } else if (_$.MAP_BLIGHT_RAVAGED.test(item.baseType)) {
        item.baseType = _$REF.MAP_BLIGHT_RAVAGED.exec(item.baseType)![1];
      }
    } else {
      if (_$.MAP_BLIGHTED.test(item.name)) {
        item.name = _$REF.MAP_BLIGHTED.exec(item.name)![1];
      } else if (_$.MAP_BLIGHT_RAVAGED.test(item.name)) {
        item.name = _$REF.MAP_BLIGHT_RAVAGED.exec(item.name)![1];
      }
    }
  }

  if (item.category === ItemCategory.MetamorphSample) {
    if (_$.METAMORPH_BRAIN.test(item.name)) {
      item.name = "Metamorph Brain";
    } else if (_$.METAMORPH_EYE.test(item.name)) {
      item.name = "Metamorph Eye";
    } else if (_$.METAMORPH_LUNG.test(item.name)) {
      item.name = "Metamorph Lung";
    } else if (_$.METAMORPH_HEART.test(item.name)) {
      item.name = "Metamorph Heart";
    } else if (_$.METAMORPH_LIVER.test(item.name)) {
      item.name = "Metamorph Liver";
    }
  }
}

function findInDatabase(item: ParserState) {
  let info: BaseType[] | undefined;
  if (item.category === ItemCategory.DivinationCard) {
    info = ITEM_BY_REF("DIVINATION_CARD", item.name);
  } else if (item.category === ItemCategory.CapturedBeast) {
    info = ITEM_BY_REF("CAPTURED_BEAST", item.baseType ?? item.name);
  } else if (item.category === ItemCategory.Gem) {
    info = ITEM_BY_REF("GEM", item.name);
  } else if (item.category === ItemCategory.MetamorphSample) {
    info = ITEM_BY_REF("ITEM", item.name);
  } else if (item.category === ItemCategory.Voidstone) {
    info = ITEM_BY_REF("ITEM", "Charged Compass");
  } else if (item.rarity === ItemRarity.Unique && !item.isUnidentified) {
    info = ITEM_BY_REF("UNIQUE", item.name);
  } else {
    info = ITEM_BY_REF("ITEM", item.baseType ?? item.name);
  }
  if (!info?.length) {
    // HACK: controller support while poe2 doesn't have advanced copy for controllers
    if (item.category === ItemCategory.DivinationCard) {
      info = ITEM_BY_TRANSLATED("DIVINATION_CARD", item.name);
    } else if (item.category === ItemCategory.CapturedBeast) {
      info = ITEM_BY_TRANSLATED("CAPTURED_BEAST", item.baseType ?? item.name);
    } else if (item.category === ItemCategory.Gem) {
      info = ITEM_BY_TRANSLATED("GEM", item.name);
    } else if (item.category === ItemCategory.MetamorphSample) {
      info = ITEM_BY_TRANSLATED("ITEM", item.name);
    } else if (item.category === ItemCategory.Voidstone) {
      info = ITEM_BY_TRANSLATED("ITEM", "Charged Compass");
    } else if (item.rarity === ItemRarity.Unique && !item.isUnidentified) {
      info = ITEM_BY_TRANSLATED("UNIQUE", item.name);
    } else {
      info = ITEM_BY_TRANSLATED("ITEM", item.baseType ?? item.name);
    }
    if (!info?.length) {
      return err("item.unknown");
    }
  }
  if (info[0].unique) {
    const uniqueInfo = info.filter(
      (info) => info.unique!.base === item.baseType,
    );
    if (uniqueInfo?.length) {
      info = uniqueInfo;
    } else if (item.baseType) {
      const baseInfo = ITEM_BY_TRANSLATED("ITEM", item.baseType);
      if (baseInfo?.length) {
        info = info.filter((info) => info.unique!.base === baseInfo[0].refName);
      }
    }
  }
  item.infoVariants = info;
  // choose 1st variant, correct one will be picked at the end of parsing
  item.info = info[0];
  // same for every variant
  if (!item.category) {
    if (item.info.craftable) {
      item.category = item.info.craftable.category;
    } else if (item.info.unique) {
      item.category = ITEM_BY_REF(
        "ITEM",
        item.info.unique.base,
      )![0].craftable!.category;
    }
  }

  // Override charm since its flask in trade
  if (item.category === ItemCategory.Charm) {
    item.category = ItemCategory.Flask;
  }
}

function parseMap(section: string[], item: ParsedItem) {
  if (section[0].startsWith(_$.MAP_TIER)) {
    item.mapTier = Number(section[0].slice(_$.MAP_TIER.length));
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseWaystone(section: string[], item: ParsedItem) {
  if (section[0].startsWith(_$.WAYSTONE_TIER)) {
    item.mapTier = Number(section[0].slice(_$.WAYSTONE_TIER.length));
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseBlightedMap(item: ParsedItem) {
  if (item.category !== ItemCategory.Map) return;

  const calc = item.statsByType.find(
    (calc) =>
      calc.type === ModifierType.Implicit &&
      calc.stat.ref.startsWith("Area is infested with Fungal Growths"),
  );
  if (calc !== undefined) {
    if (calc.sources[0].contributes!.value === 9) {
      item.mapBlighted = "Blight-ravaged";
      item.info.icon = ITEM_BY_REF("ITEM", "Blight-ravaged Map")![0].icon;
    } else {
      item.mapBlighted = "Blighted";
      item.info.icon = ITEM_BY_REF("ITEM", "Blighted Map")![0].icon;
    }
  }
}

function parseFractured(item: ParserState) {
  if (item.newMods.some((mod) => mod.info.type === ModifierType.Fractured)) {
    item.isFractured = true;
  }
}

function pickCorrectVariant(item: ParserState) {
  if (!item.info.disc) return;

  for (const variant of item.infoVariants) {
    const cond = variant.disc!;

    if (cond.propAR && !item.armourAR) continue;
    if (cond.propEV && !item.armourEV) continue;
    if (cond.propES && !item.armourES) continue;

    if (cond.mapTier === "W" && !(item.mapTier! <= 5)) continue;
    if (cond.mapTier === "Y" && !(item.mapTier! >= 6 && item.mapTier! <= 10))
      continue;
    if (cond.mapTier === "R" && !(item.mapTier! >= 11)) continue;

    if (
      cond.hasImplicit &&
      !item.statsByType.some(
        (calc) =>
          calc.type === ModifierType.Implicit &&
          calc.stat.ref === cond.hasImplicit!.ref,
      )
    )
      continue;

    if (
      cond.hasExplicit &&
      !item.statsByType.some(
        (calc) =>
          calc.type === ModifierType.Explicit &&
          calc.stat.ref === cond.hasExplicit!.ref,
      )
    )
      continue;

    if (cond.sectionText && !item.rawText.includes(cond.sectionText)) continue;

    item.info = variant;
  }

  // it may happen that we don't find correct variant
  // i.e. corrupted implicit on Two-Stone Ring
}

function parseNamePlate(section: string[]) {
  let line = section.shift();
  if (!line?.startsWith(_$.ITEM_CLASS)) {
    return err("item.parse_error");
  }

  line = section.shift();
  let rarityText: string | undefined;
  if (line?.startsWith(_$.RARITY)) {
    rarityText = line.slice(_$.RARITY.length);
    line = section.shift();
  }

  let name: string;
  if (line != null) {
    name = markupConditionParser(line);
  } else {
    return err("item.parse_error");
  }

  line = section.shift();
  const baseType = line && markupConditionParser(line);

  const item: ParserState = {
    rarity: undefined,
    category: undefined,
    name,
    baseType,
    isUnidentified: false,
    isCorrupted: false,
    newMods: [],
    statsByType: [],
    unknownModifiers: [],
    influences: [],
    info: undefined!,
    infoVariants: undefined!,
    rawText: undefined!,
  };

  switch (rarityText) {
    case _$.RARITY_CURRENCY:
      item.category = ItemCategory.Currency;
      break;
    case _$.RARITY_DIVCARD:
      item.category = ItemCategory.DivinationCard;
      break;
    case _$.RARITY_GEM:
      item.category = ItemCategory.Gem;
      break;
    case _$.RARITY_NORMAL:
    case _$.RARITY_QUEST:
      item.rarity = ItemRarity.Normal;
      break;
    case _$.RARITY_MAGIC:
      item.rarity = ItemRarity.Magic;
      break;
    case _$.RARITY_RARE:
      item.rarity = ItemRarity.Rare;
      break;
    case _$.RARITY_UNIQUE:
      item.rarity = ItemRarity.Unique;
      break;
  }

  return ok(item);
}

function parseInfluence(section: string[], item: ParsedItem) {
  if (section.length <= 2) {
    const countBefore = item.influences.length;

    for (const line of section) {
      switch (line) {
        case _$.INFLUENCE_CRUSADER:
          item.influences.push(ItemInfluence.Crusader);
          break;
        case _$.INFLUENCE_ELDER:
          item.influences.push(ItemInfluence.Elder);
          break;
        case _$.INFLUENCE_SHAPER:
          item.influences.push(ItemInfluence.Shaper);
          break;
        case _$.INFLUENCE_HUNTER:
          item.influences.push(ItemInfluence.Hunter);
          break;
        case _$.INFLUENCE_REDEEMER:
          item.influences.push(ItemInfluence.Redeemer);
          break;
        case _$.INFLUENCE_WARLORD:
          item.influences.push(ItemInfluence.Warlord);
          break;
      }
    }

    if (countBefore < item.influences.length) {
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

// #region Small Sections
function parseCorrupted(section: string[], item: ParsedItem) {
  if (section[0].trim() === _$.CORRUPTED) {
    item.isCorrupted = true;
    return "SECTION_PARSED";
  } else if (section[0] === _$.UNMODIFIABLE) {
    item.isCorrupted = true;
    item.isUnmodifiable = true;
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseFoil(section: string[], item: ParsedItem) {
  if (item.rarity !== ItemRarity.Unique) {
    return "PARSER_SKIPPED";
  }
  if (section[0] === _$.FOIL_UNIQUE) {
    item.isFoil = true;
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseUnidentified(section: string[], item: ParsedItem) {
  if (section[0] === _$.UNIDENTIFIED) {
    item.isUnidentified = true;
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseItemLevel(section: string[], item: ParsedItem) {
  let prefix = _$.ITEM_LEVEL;
  if (item.info.refName === "Filled Coffin") {
    prefix = _$.CORPSE_LEVEL;
  }

  for (const line of section) {
    if (line.startsWith(prefix)) {
      item.itemLevel = Number(line.slice(prefix.length));
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseRequirements(section: string[], item: ParsedItem) {
  if (
    section[0].startsWith(_$.REQUIREMENTS) ||
    section[0].startsWith(_$.REQUIRES)
  ) {
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseTalismanTier(section: string[], item: ParsedItem) {
  if (section[0].startsWith(_$.TALISMAN_TIER)) {
    item.talismanTier = Number(section[0].slice(_$.TALISMAN_TIER.length));
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseVaalGemName(section: string[], item: ParserState) {
  if (item.category !== ItemCategory.Gem) return "PARSER_SKIPPED";

  // TODO blocked by https://www.pathofexile.com/forum/view-thread/3231236
  if (section.length === 1) {
    let gemName: string | undefined;
    if (ITEM_BY_REF("GEM", section[0])) {
      gemName = section[0];
    }
    if (gemName) {
      item.name = ITEM_BY_REF("GEM", gemName)![0].refName;
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseGem(section: string[], item: ParsedItem) {
  if (
    item.category !== ItemCategory.Gem &&
    item.category !== ItemCategory.UncutGem
  ) {
    return "PARSER_SKIPPED";
  }

  const gemLevelLineNumber = item.category === ItemCategory.Gem ? 1 : 0;

  if (section[gemLevelLineNumber]?.startsWith(_$.GEM_LEVEL)) {
    // "Level: 20 (Max)"
    item.gemLevel = parseInt(
      section[gemLevelLineNumber].slice(_$.GEM_LEVEL.length),
      10,
    );

    parseQualityNested(section, item);

    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}
// #endregion

function parseStackSize(section: string[], item: ParsedItem) {
  if (
    item.rarity !== ItemRarity.Normal &&
    item.category !== ItemCategory.Currency &&
    item.category !== ItemCategory.DivinationCard &&
    item.category !== ItemCategory.MapFragment
  ) {
    return "PARSER_SKIPPED";
  }
  if (section[0].startsWith(_$.STACK_SIZE)) {
    // Portal Scroll "Stack Size: 2[localized separator]448/40"
    const [value, max] = section[0]
      .slice(_$.STACK_SIZE.length)
      .replace(/[^\d/]/g, "")
      .split("/")
      .map(Number);
    if (item.info.refName !== "Idol of Estazunti") {
      item.stackSize = { value, max };
    }

    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseRuneSockets(section: string[], item: ParsedItem) {
  const categoryMax = getMaxSockets(item);
  const armourOrWeapon =
    categoryMax &&
    (isArmourOrWeaponOrCaster(item.category) ||
      item.info.refName === "Darkness Enthroned");
  if (!armourOrWeapon) return "PARSER_SKIPPED";
  if (section[0].startsWith(_$.SOCKETS)) {
    const sockets = section[0].slice(_$.SOCKETS.length).trimEnd();
    const current = sockets.split("S").length - 1;
    if (!itemIsModifiable(item)) {
      item.runeSockets = {
        empty: 0,
        current,
        normal: categoryMax,
      };
    } else {
      item.runeSockets = {
        empty: 0,
        current,
        normal: categoryMax,
      };
    }

    return "SECTION_PARSED";
  }
  if (categoryMax && itemIsModifiable(item)) {
    item.runeSockets = {
      empty: categoryMax,
      current: 0,
      normal: categoryMax,
    };
  }
  return "SECTION_SKIPPED";
}

function parseSockets(section: string[], item: ParsedItem) {
  if (item.category === ItemCategory.Gem && section[0].startsWith(_$.SOCKETS)) {
    let sockets = section[0].slice(_$.SOCKETS.length).trimEnd();
    sockets = sockets.replace(/[^ -]/g, "#");

    item.gemSockets = {
      number: sockets.split("#").length - 1,
      white: sockets.split("W").length - 1,
      linked: undefined,
    };

    if (sockets === "#-#-#-#-#-#") {
      item.gemSockets.linked = 6;
    } else if (
      sockets === "# #-#-#-#-#" ||
      sockets === "#-#-#-#-# #" ||
      sockets === "#-#-#-#-#"
    ) {
      item.gemSockets.linked = 5;
    }
    return "SECTION_PARSED";
  }
  return "SECTION_SKIPPED";
}

function parseQualityNested(section: string[], item: ParsedItem) {
  for (const line of section) {
    if (line.startsWith(_$.QUALITY)) {
      // "Quality: +20% (augmented)"
      item.quality = parseInt(line.slice(_$.QUALITY.length), 10);
      break;
    }
  }
}

function parseArmour(section: string[], item: ParsedItem) {
  let isParsed: SectionParseResult = "SECTION_SKIPPED";

  for (const line of section) {
    if (line.startsWith(_$.ARMOUR)) {
      item.armourAR = parseInt(line.slice(_$.ARMOUR.length), 10);
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.EVASION)) {
      item.armourEV = parseInt(line.slice(_$.EVASION.length), 10);
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.ENERGY_SHIELD)) {
      item.armourES = parseInt(line.slice(_$.ENERGY_SHIELD.length), 10);
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.BLOCK_CHANCE)) {
      item.armourBLOCK = parseInt(line.slice(_$.BLOCK_CHANCE.length), 10);
      isParsed = "SECTION_PARSED";
      continue;
    }
  }

  if (isParsed === "SECTION_PARSED") {
    parseQualityNested(section, item);
  }
  if (item.rarity === "Unique") {
    // undo everything
    item.armourAR = undefined;
    item.armourEV = undefined;
    item.armourES = undefined;
    item.armourBLOCK = undefined;
  }

  return isParsed;
}

function parseWeapon(section: string[], item: ParsedItem) {
  let isParsed: SectionParseResult = "SECTION_SKIPPED";

  for (const line of section) {
    if (line.startsWith(_$.CRIT_CHANCE)) {
      // No regex since it can have decimals
      item.weaponCRIT = parseFloat(line.slice(_$.CRIT_CHANCE.length));
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.ATTACK_SPEED)) {
      // No regex since it can have decimals
      item.weaponAS = parseFloat(line.slice(_$.ATTACK_SPEED.length));
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.PHYSICAL_DAMAGE)) {
      item.weaponPHYSICAL = getRollOrMinmaxAvg(
        line
          .slice(_$.PHYSICAL_DAMAGE.length)
          .split(_$.HYPHEN)
          .map((str) => parseInt(str.replace(/[^\d]/g, ""), 10)),
      );
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.ELEMENTAL_DAMAGE)) {
      item.weaponELEMENTAL = line
        .slice(_$.ELEMENTAL_DAMAGE.length)
        .split(", ")
        .map((element) =>
          getRollOrMinmaxAvg(
            element
              .split(_$.HYPHEN)
              .map((str) => parseInt(str.replace(/[^\d]/g, ""), 10)),
          ),
        )
        .reduce((sum, x) => sum + x, 0);

      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.FIRE_DAMAGE)) {
      const fireDamage = line
        .slice(_$.FIRE_DAMAGE.length)
        .split(", ")
        .map((element) =>
          getRollOrMinmaxAvg(
            element
              .split(_$.HYPHEN)
              .map((str) => parseInt(str.replace(/[^\d]/g, ""), 10)),
          ),
        )
        .reduce((sum, x) => sum + x, 0);
      if (item.weaponELEMENTAL) {
        item.weaponELEMENTAL = fireDamage + item.weaponELEMENTAL;
      } else {
        item.weaponELEMENTAL = fireDamage;
      }
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.COLD_DAMAGE)) {
      const coldDamage = line
        .slice(_$.COLD_DAMAGE.length)
        .split(", ")
        .map((element) =>
          getRollOrMinmaxAvg(
            element
              .split(_$.HYPHEN)
              .map((str) => parseInt(str.replace(/[^\d]/g, ""), 10)),
          ),
        )
        .reduce((sum, x) => sum + x, 0);
      if (item.weaponELEMENTAL) {
        item.weaponELEMENTAL = coldDamage + item.weaponELEMENTAL;
      } else {
        item.weaponELEMENTAL = coldDamage;
      }
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.LIGHTNING_DAMAGE)) {
      const lightningDamage = line
        .slice(_$.LIGHTNING_DAMAGE.length)
        .split(", ")
        .map((element) =>
          getRollOrMinmaxAvg(
            element
              .split(_$.HYPHEN)
              .map((str) => parseInt(str.replace(/[^\d]/g, ""), 10)),
          ),
        )
        .reduce((sum, x) => sum + x, 0);
      if (item.weaponELEMENTAL) {
        item.weaponELEMENTAL = lightningDamage + item.weaponELEMENTAL;
      } else {
        item.weaponELEMENTAL = lightningDamage;
      }
      isParsed = "SECTION_PARSED";
      continue;
    }
    if (line.startsWith(_$.RELOAD_SPEED)) {
      // No regex since it can have decimals
      item.weaponReload = parseFloat(line.slice(_$.RELOAD_SPEED.length));
      isParsed = "SECTION_PARSED";
      continue;
    }
  }

  if (isParsed === "SECTION_PARSED") {
    parseQualityNested(section, item);
  }

  if (item.rarity === "Unique") {
    // undo everything
    item.weaponELEMENTAL = undefined;
    item.weaponAS = undefined;
    item.weaponPHYSICAL = undefined;
    item.weaponCOLD = undefined;
    item.weaponLIGHTNING = undefined;
    item.weaponFIRE = undefined;
    item.weaponCRIT = undefined;
    item.weaponReload = undefined;
  }

  return isParsed;
}

function parseCaster(section: string[], item: ParsedItem) {
  if (
    item.category !== ItemCategory.Wand &&
    item.category !== ItemCategory.Sceptre &&
    item.category !== ItemCategory.Staff
  )
    return "PARSER_SKIPPED";

  if (section.length === 1 && section[0].startsWith(_$.QUALITY)) {
    parseQualityNested(section, item);
    return "SECTION_PARSED";
  }

  return "SECTION_SKIPPED";
}

function parseLogbookArea(section: string[], item: ParsedItem) {
  if (item.info.refName !== "Expedition Logbook") return "PARSER_SKIPPED";
  if (section.length < 3) return "SECTION_SKIPPED";

  // skip Area, parse Faction
  const faction = STAT_BY_MATCH_STR(section[1]);
  if (!faction) return "SECTION_SKIPPED";

  const areaMods: ParsedModifier[] = [
    {
      info: { tags: [], type: ModifierType.Pseudo },
      stats: [
        {
          stat: faction.stat,
          translation: faction.matcher,
        },
      ],
    },
  ];

  const { modType, lines } = parseModType(section.slice(2));
  for (const line of lines) {
    const found = tryParseTranslation(
      { string: line, unscalable: false },
      modType,
    );
    if (found) {
      areaMods.push({
        info: { tags: [], type: modType },
        stats: [found],
      });
    }
  }

  areaMods.shift();
  if (areaMods.length) {
    if (!item.logbookAreaMods) {
      item.logbookAreaMods = [areaMods];
    } else {
      item.logbookAreaMods.push(areaMods);
    }
  }

  return "SECTION_PARSED";
}

export function parseModifiersPoe2(section: string[], item: ParsedItem) {
  if (
    item.rarity !== ItemRarity.Normal &&
    item.rarity !== ItemRarity.Magic &&
    item.rarity !== ItemRarity.Rare &&
    item.rarity !== ItemRarity.Unique
  ) {
    return "PARSER_SKIPPED";
  }

  let foundAnyMods = false;

  const hasEndingTag = section.find(
    (line) =>
      line.endsWith(ENCHANT_LINE) ||
      line.endsWith(SCOURGE_LINE) ||
      line.endsWith(RUNE_LINE) ||
      line.endsWith(ADDED_RUNE_LINE) ||
      line.startsWith(_$.GRANTS_SKILL),
  );

  if (hasEndingTag) {
    const { lines } = parseModType(section);
    let modType;

    if (hasEndingTag.endsWith(ENCHANT_LINE)) {
      modType = ModifierType.Enchant;
    } else if (hasEndingTag.endsWith(SCOURGE_LINE)) {
      modType = ModifierType.Scourge;
    } else if (hasEndingTag.endsWith(ADDED_RUNE_LINE)) {
      modType = ModifierType.AddedRune;
    } else if (hasEndingTag.endsWith(RUNE_LINE)) {
      modType = ModifierType.Rune;
    } else if (hasEndingTag.startsWith(_$.GRANTS_SKILL)) {
      modType = ModifierType.Skill;
    } else {
      throw new Error("Invalid ending tag");
    }
    const modInfo: ModifierInfo = {
      type: modType,
      tags: [],
    };
    foundAnyMods = parseStatsFromMod(lines, item, { info: modInfo, stats: [] });
  } else {
    for (const statLines of section) {
      let { modType, lines } = parseModType([statLines]);
      if (
        modType === ModifierType.Explicit &&
        item.category === ItemCategory.Relic
      ) {
        modType = ModifierType.Sanctum;
      }
      // const modInfo = parseModInfoLine(modLine, modType);
      const found = parseStatsFromMod(lines, item, {
        info: { type: modType, tags: [] },
        stats: [],
      });
      foundAnyMods = found || foundAnyMods;

      if (modType === ModifierType.Veiled) {
        item.isVeiled = true;
      }
    }
  }

  return foundAnyMods ? "SECTION_PARSED" : "SECTION_SKIPPED";
}

function parseModifiers(section: string[], item: ParsedItem) {
  if (
    item.rarity !== ItemRarity.Normal &&
    item.rarity !== ItemRarity.Magic &&
    item.rarity !== ItemRarity.Rare &&
    item.rarity !== ItemRarity.Unique
  ) {
    return "PARSER_SKIPPED";
  }

  const recognizedLine = section.find(
    (line) =>
      line.endsWith(ENCHANT_LINE) ||
      line.endsWith(RUNE_LINE) ||
      line.startsWith(_$.GRANTS_SKILL) ||
      isModInfoLine(line),
  );

  if (!recognizedLine) {
    return "SECTION_SKIPPED";
  }

  if (isModInfoLine(recognizedLine)) {
    for (const { modLine, statLines } of groupLinesByMod(section)) {
      const { modType, lines } = parseModType(statLines);

      const modInfo = parseModInfoLine(modLine, modType);
      if (
        item.category === ItemCategory.Relic &&
        modInfo.type === ModifierType.Explicit
      ) {
        modInfo.type = ModifierType.Sanctum;
      }
      parseStatsFromMod(lines, item, { info: modInfo, stats: [] });

      if (modType === ModifierType.Veiled) {
        item.isVeiled = true;
      }
    }
  } else {
    const { lines } = parseModType(section);
    const modInfo: ModifierInfo = {
      type: recognizedLine.endsWith(ENCHANT_LINE)
        ? ModifierType.Enchant
        : recognizedLine.startsWith(_$.GRANTS_SKILL)
          ? ModifierType.Skill
          : ModifierType.Rune,
      tags: [],
    };
    parseStatsFromMod(lines, item, { info: modInfo, stats: [] });
  }

  return "SECTION_PARSED";
}

function applyRuneSockets(item: ParsedItem) {
  // If we have any rune sockets
  if (item.runeSockets) {
    // Count current mods that are of type Rune

    const runeMods = item.newMods.filter(
      (mod) => mod.info.type === ModifierType.Rune,
    );
    const runeStats = item.statsByType.filter(
      (calc) => calc.type === ModifierType.Rune,
    );
    const runes = runeMods
      .map((mod) => {
        const stat = runeStats.find(
          (stat) => stat.sources[0].stat === mod.stats[0],
        );
        if (!stat) return [];
        return runeCount(mod, stat);
      })
      .flat();

    // HACK: fix since I can't detect how many exist due to rune tiers
    const tempFix = runes.reduce((x, y) => x + y, 0) > 0;
    const potentialEmptySockets = tempFix
      ? 0
      : Math.max(item.runeSockets.normal, item.runeSockets.current);
    item.runeSockets.empty = potentialEmptySockets;
  }
}

function parseMirrored(section: string[], item: ParsedItem) {
  if (section.length === 1) {
    if (section[0] === _$.MIRRORED) {
      item.isMirrored = true;
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseSanctified(section: string[], item: ParsedItem) {
  if (section.length === 1) {
    if (section[0] === _$.SANCTIFIED) {
      item.isSanctified = true;
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseFlask(section: string[], item: ParsedItem) {
  // the purpose of this parser is to "consume" flask buffs
  // so they are not recognized as modifiers

  let isParsed: SectionParseResult = "SECTION_SKIPPED";

  for (const line of section) {
    if (_$.FLASK_CHARGES.test(line)) {
      isParsed = "SECTION_PARSED";
      break;
    }
  }

  if (isParsed) {
    parseQualityNested(section, item);
  }

  return isParsed;
}

function parseJewelery(section: string[], item: ParsedItem) {
  if (
    item.category !== ItemCategory.Amulet &&
    item.category !== ItemCategory.Ring &&
    item.category !== ItemCategory.Belt
  ) {
    return "PARSER_SKIPPED";
  }

  for (const line of section) {
    if (line.startsWith(_$.QUALITY.substring(0, _$.QUALITY.indexOf(":")))) {
      return "SECTION_PARSED";
    }
  }

  return "SECTION_SKIPPED";
}

function parseCharmSlots(section: string[], item: ParsedItem) {
  // the purpose of this parser is to "consume" charm slot 1 sections
  // so they are not recognized as modifiers
  if (item.category !== ItemCategory.Belt) return "PARSER_SKIPPED";

  let isParsed: SectionParseResult = "SECTION_SKIPPED";

  for (const line of section) {
    if (line.startsWith(_$.CHARM_SLOTS)) {
      isParsed = "SECTION_PARSED";
      break;
    }
  }

  return isParsed;
}

function parseSpirit(section: string[], item: ParsedItem) {
  // the purpose of this parser is to "consume" Spirit: 100 sections
  // so they are not recognized as modifiers
  if (item.category !== ItemCategory.Sceptre) return "PARSER_SKIPPED";

  let isParsed: SectionParseResult = "SECTION_SKIPPED";

  for (const line of section) {
    if (line.startsWith(_$.BASE_SPIRIT)) {
      isParsed = "SECTION_PARSED";
      break;
    }
  }

  return isParsed;
}

function parsePriceNote(section: string[], item: ParsedItem) {
  for (const line of section) {
    if (line.startsWith(_$.PRICE_NOTE)) {
      item.note = line.slice(_$.PRICE_NOTE.length);
      return "SECTION_PARSED";
    }
  }

  return "SECTION_SKIPPED";
}

function parseFracturedText(section: string[], _item: ParsedItem) {
  for (const line of section) {
    if (line === _$.FRACTURED_ITEM) {
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseUnneededText(section: string[], item: ParsedItem) {
  if (
    item.category !== ItemCategory.Quiver &&
    item.category !== ItemCategory.Flask &&
    item.category !== ItemCategory.Charm &&
    item.category !== ItemCategory.Waystone &&
    item.category !== ItemCategory.Map &&
    item.category !== ItemCategory.Jewel &&
    item.category !== ItemCategory.Relic &&
    item.category !== ItemCategory.Tablet &&
    item.info.refName !== "Expedition Logbook" &&
    item.category !== ItemCategory.Shield &&
    item.category !== ItemCategory.Spear &&
    item.category !== ItemCategory.Buckler
  ) {
    return "PARSER_SKIPPED";
  }

  for (const line of section) {
    if (
      line.startsWith(_$.QUIVER_HELP_TEXT) ||
      line.startsWith(_$.FLASK_HELP_TEXT) ||
      line.startsWith(_$.CHARM_HELP_TEXT) ||
      line.startsWith(_$.WAYSTONE_HELP) ||
      line.startsWith(_$.JEWEL_HELP) ||
      line.startsWith(_$.SANCTUM_HELP) ||
      line.startsWith(_$.PRECURSOR_TABLET_HELP) ||
      line.startsWith(_$.LOGBOOK_HELP) ||
      line.startsWith(_$.GRANTS_SKILL)
    ) {
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseTimelostRadius(section: string[], item: ParsedItem) {
  if (item.category !== ItemCategory.Jewel) return "PARSER_SKIPPED";
  for (const line of section) {
    if (line.startsWith(_$.TIMELESS_RADIUS)) {
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseSentinelCharge(section: string[], item: ParsedItem) {
  if (item.category !== ItemCategory.Sentinel) return "PARSER_SKIPPED";

  if (section.length === 1) {
    if (section[0].startsWith(_$.SENTINEL_CHARGE)) {
      item.sentinelCharge = parseInt(
        section[0].slice(_$.SENTINEL_CHARGE.length),
        10,
      );
      return "SECTION_PARSED";
    }
  }
  return "SECTION_SKIPPED";
}

function parseSynthesised(section: string[], item: ParserState) {
  if (section.length === 1) {
    if (section[0] === _$.SECTION_SYNTHESISED) {
      item.isSynthesised = true;
      if (item.baseType) {
        item.baseType = _$REF.ITEM_SYNTHESISED.exec(item.baseType)![1];
      } else {
        item.name = _$REF.ITEM_SYNTHESISED.exec(item.name)![1];
      }
      return "SECTION_PARSED";
    }
  }

  return "SECTION_SKIPPED";
}

function parseSuperior(item: ParserState) {
  if (
    item.rarity === ItemRarity.Normal ||
    (item.rarity === ItemRarity.Magic && item.isUnidentified) ||
    (item.rarity === ItemRarity.Rare && item.isUnidentified) ||
    (item.rarity === ItemRarity.Unique && item.isUnidentified)
  ) {
    if (_$REF.ITEM_SUPERIOR.test(item.name)) {
      item.name = _$REF.ITEM_SUPERIOR.exec(item.name)![1];
    }
  }
}

function parseExceptional(item: ParserState) {
  if (
    item.rarity === ItemRarity.Normal ||
    (item.rarity === ItemRarity.Magic && item.isUnidentified) ||
    (item.rarity === ItemRarity.Rare && item.isUnidentified) ||
    (item.rarity === ItemRarity.Unique && item.isUnidentified)
  ) {
    if (_$REF.ITEM_EXCEPTIONAL.test(item.name)) {
      item.name = _$REF.ITEM_EXCEPTIONAL.exec(item.name)![1];
    }
  }
}

function parseCategoryByHelpText(section: string[], item: ParsedItem) {
  if (section[0] === _$.BEAST_HELP) {
    item.category = ItemCategory.CapturedBeast;
    return "SECTION_PARSED";
  } else if (section[0] === _$.METAMORPH_HELP) {
    item.category = ItemCategory.MetamorphSample;
    return "SECTION_PARSED";
  } else if (section[0] === _$.VOIDSTONE_HELP) {
    item.category = ItemCategory.Voidstone;
    return "SECTION_PARSED";
  }

  return "SECTION_SKIPPED";
}

function parseHeistBlueprint(section: string[], item: ParsedItem) {
  if (item.category !== ItemCategory.HeistBlueprint) return "PARSER_SKIPPED";

  parseAreaLevelNested(section, item);
  if (!item.areaLevel) {
    return "SECTION_SKIPPED";
  }

  item.heist = {};

  for (const line of section) {
    if (line.startsWith(_$.HEIST_TARGET)) {
      const targetText = line.slice(_$.HEIST_TARGET.length);
      switch (targetText) {
        case _$.HEIST_BLUEPRINT_ENCHANTS:
          item.heist.target = "Enchants";
          break;
        case _$.HEIST_BLUEPRINT_GEMS:
          item.heist.target = "Gems";
          break;
        case _$.HEIST_BLUEPRINT_REPLICAS:
          item.heist.target = "Replicas";
          break;
        case _$.HEIST_BLUEPRINT_TRINKETS:
          item.heist.target = "Trinkets";
          break;
      }
    } else if (line.startsWith(_$.HEIST_WINGS_REVEALED)) {
      item.heist.wingsRevealed = parseInt(
        line.slice(_$.HEIST_WINGS_REVEALED.length),
        10,
      );
    }
  }

  return "SECTION_PARSED";
}

function parseAreaLevelNested(section: string[], item: ParsedItem) {
  for (const line of section) {
    if (line.startsWith(_$.AREA_LEVEL)) {
      item.areaLevel = Number(line.slice(_$.AREA_LEVEL.length));
      break;
    }
  }
}

function parseAreaLevel(section: string[], item: ParsedItem) {
  if (
    item.info.refName !== "Chronicle of Atzoatl" &&
    item.info.refName !== "Expedition Logbook" &&
    item.info.refName !== "Mirrored Tablet" &&
    item.info.refName !== "Forbidden Tome"
  )
    return "PARSER_SKIPPED";

  parseAreaLevelNested(section, item);

  return item.areaLevel ? "SECTION_PARSED" : "SECTION_SKIPPED";
}

function parseAtzoatlRooms(section: string[], item: ParsedItem) {
  if (item.info.refName !== "Chronicle of Atzoatl") return "PARSER_SKIPPED";
  if (section[0] !== _$.INCURSION_OPEN) return "SECTION_SKIPPED";

  let state = IncursionRoom.Open;
  for (const line of section.slice(1)) {
    if (line === _$.INCURSION_OBSTRUCTED) {
      state = IncursionRoom.Obstructed;
      continue;
    }

    const found = STAT_BY_MATCH_STR(line);
    if (found) {
      item.newMods.push({
        info: { tags: [], type: ModifierType.Pseudo },
        stats: [
          {
            stat: found.stat,
            translation: {
              string:
                state === IncursionRoom.Open
                  ? found.matcher.string
                  : `${_$.INCURSION_OBSTRUCTED} ${found.matcher.string}`,
            },
            roll: {
              value: state,
              min: state,
              max: state,
              dp: false,
              unscalable: true,
            },
          },
        ],
      });
    } else {
      item.unknownModifiers.push({
        text: line,
        type: ModifierType.Pseudo,
      });
    }
  }

  return "SECTION_PARSED";
}

function parseMirroredTablet(section: string[], item: ParsedItem) {
  if (item.info.refName !== "Mirrored Tablet") return "PARSER_SKIPPED";
  if (section.length < 8) return "SECTION_SKIPPED";

  for (const line of section) {
    const found = tryParseTranslation(
      { string: line, unscalable: true },
      ModifierType.Pseudo,
    );
    if (found) {
      item.newMods.push({
        info: { tags: [], type: ModifierType.Pseudo },
        stats: [found],
      });
    } else {
      item.unknownModifiers.push({
        text: line,
        type: ModifierType.Pseudo,
      });
    }
  }

  return "SECTION_PARSED";
}

function parseFilledCoffin(section: string[], item: ParsedItem) {
  if (item.info.refName !== "Filled Coffin") return "PARSER_SKIPPED";
  if (!section.some((line) => line.endsWith(IMPLICIT_LINE)))
    return "SECTION_SKIPPED";

  const { lines } = parseModType(section);
  const modInfo: ModifierInfo = {
    type: ModifierType.Necropolis,
    tags: [],
  };
  parseStatsFromMod(lines, item, { info: modInfo, stats: [] });

  return "SECTION_PARSED";
}

function markupConditionParser(text: string) {
  // ignores state set by <<set:__>>
  // always evaluates first condition to true <if:__>{...}
  // full markup: https://gist.github.com/SnosMe/151549b532df8ea08025a76ae2920ca4

  text = text.replace(/<<set:.+?>>/g, "");
  text = text.replace(
    /<(if:.+?|elif:.+?|else)>{(.+?)}/g,
    (_, type: string, body: string) => {
      return type.startsWith("if:") ? body : "";
    },
  );

  return text;
}

function parseStatsFromMod(
  lines: string[],
  item: ParsedItem,
  modifier: ParsedModifier,
): boolean {
  item.newMods.push(modifier);

  if (modifier.info.type === ModifierType.Veiled) {
    return true;
  }

  const statIterator = linesToStatStrings(lines);
  let stat = statIterator.next();
  while (!stat.done) {
    if (item.info.refName === "From Nothing") {
      stat.value.string = stat.value.string.replace("()", "");
    }

    const parsedStat = tryParseTranslation(stat.value, modifier.info.type);
    if (parsedStat) {
      modifier.stats.push(parsedStat);

      stat = statIterator.next(true);
    } else {
      stat = statIterator.next(false);
    }
  }

  if (item.rarity !== ItemRarity.Unique) {
    item.unknownModifiers.push(
      ...stat.value.map((line) => ({
        text: line,
        type: modifier.info.type,
      })),
    );
  }
  return true;
}

/**
 * @deprecated
 */
function transformToLegacyModifiers(item: ParsedItem) {
  item.statsByType = sumStatsByModType(item.newMods);
}

function applyElementalAdded(item: ParsedItem) {
  if (item.weaponELEMENTAL && item.rarity !== "Unique") {
    const knownRefs = new Set<string>([
      "Adds # to # Lightning Damage",
      "Adds # to # Cold Damage",
      "Adds # to # Fire Damage",
    ]);

    item.statsByType.forEach((calc) => {
      if (knownRefs.has(calc.stat.ref)) {
        for (const source of calc.sources) {
          if (calc.stat.ref === "Adds # to # Lightning Damage") {
            if (item.weaponLIGHTNING) {
              item.weaponLIGHTNING =
                source.contributes!.value + item.weaponLIGHTNING;
            } else {
              item.weaponLIGHTNING = source.contributes!.value;
            }
          } else if (calc.stat.ref === "Adds # to # Cold Damage") {
            if (item.weaponCOLD) {
              item.weaponCOLD = source.contributes!.value + item.weaponCOLD;
            } else {
              item.weaponCOLD = source.contributes!.value;
            }
          } else if (calc.stat.ref === "Adds # to # Fire Damage") {
            if (item.weaponFIRE) {
              item.weaponFIRE = source.contributes!.value + item.weaponFIRE;
            } else {
              item.weaponFIRE = source.contributes!.value;
            }
          }
        }
      }
    });
  }
}

function calcBasePercentile(item: ParsedItem) {
  const info = item.info.unique
    ? ITEM_BY_REF("ITEM", item.info.unique.base)![0].armour
    : item.info.armour;
  if (!info) return;

  // Base percentile is the same for all defences.
  // Using `AR/EV -> ES` order to improve accuracy
  // of calculation (larger rolls = more precise).
  if (item.armourAR && info.ar) {
    item.basePercentile = calcPropPercentile(
      item.armourAR,
      info.ar,
      QUALITY_STATS.ARMOUR,
      item,
    );
  } else if (item.armourEV && info.ev) {
    item.basePercentile = calcPropPercentile(
      item.armourEV,
      info.ev,
      QUALITY_STATS.EVASION,
      item,
    );
  } else if (item.armourES && info.es) {
    item.basePercentile = calcPropPercentile(
      item.armourES,
      info.es,
      QUALITY_STATS.ENERGY_SHIELD,
      item,
    );
  }
}

export function removeLinesEnding(
  lines: readonly string[],
  ending: string,
): string[] {
  return lines.map((line) =>
    line.endsWith(ending) ? line.slice(0, -ending.length) : line,
  );
}

export function parseAffixStrings(clipboard: string): string {
  return clipboard.replace(/\[([^\]|]+)\|?([^\]]*)\]/g, (_, part1, part2) => {
    return part2 || part1;
  });
}
export function getMaxSockets(item: ParsedItem) {
  if (item.info.refName === "Darkness Enthroned") {
    return 2;
  }

  const { category } = item;
  switch (category) {
    case ItemCategory.BodyArmour:
    case ItemCategory.TwoHandedAxe:
    case ItemCategory.TwoHandedMace:
    case ItemCategory.TwoHandedSword:
    case ItemCategory.Crossbow:
    case ItemCategory.Bow:
    case ItemCategory.Warstaff:
    case ItemCategory.Staff:
      return 2;
    case ItemCategory.Helmet:
    case ItemCategory.Shield:
    case ItemCategory.Gloves:
    case ItemCategory.Boots:
    case ItemCategory.OneHandedAxe:
    case ItemCategory.OneHandedMace:
    case ItemCategory.OneHandedSword:
    case ItemCategory.Claw:
    case ItemCategory.Dagger:
    case ItemCategory.Focus:
    case ItemCategory.Spear:
    case ItemCategory.Flail:
    case ItemCategory.Wand:
    case ItemCategory.Buckler:
    case ItemCategory.Sceptre:
      return 1;
    default:
      return 0;
  }
}

export function isArmourOrWeaponOrCaster(
  category: ItemCategory | undefined,
): "armour" | "weapon" | "caster" | undefined {
  switch (category) {
    case ItemCategory.BodyArmour:
    case ItemCategory.Boots:
    case ItemCategory.Gloves:
    case ItemCategory.Helmet:
    case ItemCategory.Shield:
    case ItemCategory.Focus:
    case ItemCategory.Buckler:
      return "armour";
    case ItemCategory.OneHandedAxe:
    case ItemCategory.OneHandedMace:
    case ItemCategory.OneHandedSword:
    case ItemCategory.Quiver:
    case ItemCategory.Claw:
    case ItemCategory.Dagger:
    case ItemCategory.Sceptre:
    case ItemCategory.TwoHandedAxe:
    case ItemCategory.TwoHandedMace:
    case ItemCategory.TwoHandedSword:
    case ItemCategory.Crossbow:
    case ItemCategory.Bow:
    case ItemCategory.Warstaff:
    case ItemCategory.Spear:
    case ItemCategory.Flail:
      return "weapon";
    case ItemCategory.Wand:
    case ItemCategory.Staff:
      return "caster";
    default:
      return undefined;
  }
}

function runeCount(mod: ParsedModifier, statCalc: StatCalculated): number {
  if (mod.info.type !== ModifierType.Rune) return 0;
  // HACK: fix since I can't detect how many exist due to rune tiers
  // const runeTradeId = statCalc.stat.trade.ids[ModifierType.Rune][0];
  // const runeSingle = RUNE_SINGLE_VALUE[runeTradeId];

  // // Calculate how many of this rune are in the item
  // const runeAppliedValue = statCalc.sources[0].contributes!.value;
  // const runeSingleValue = runeSingle.values[0];
  // const totalRunes = Math.floor(runeAppliedValue / runeSingleValue);

  return 1;
}

export function replaceHashWithValues(template: string, values: number[]) {
  let result = template;
  values.forEach((value: number) => {
    result = result.replace("#", value.toString()); // Replace the first occurrence of #
  });
  return result;
}

function isUncutSkillGem(section: string[]): boolean {
  if (section.length !== 2) return false;
  const translated = _$.RARITY + _$.RARITY_CURRENCY;
  return section[0] === translated && section[1] !== undefined;
}

// Disable since this is export for tests
// eslint-disable-next-line @typescript-eslint/naming-convention
export const __testExports = {
  itemTextToSections,
  parseNamePlate,
  isUncutSkillGem,
  parseWeapon,
  parseArmour,
  parseModifiers,
};
